package ca.uvic.group2.server;

import ca.uvic.group2.ServerFactory;
import ca.uvic.group2.api.*;
import ca.uvic.group2.api.exceptions.*;
import java.io.Serializable;
import java.rmi.RemoteException;
import java.rmi.registry.*;
import java.rmi.server.UnicastRemoteObject;
import java.util.*;
import java.io.*;
import org.apache.log4j.*;
import java.net.*;

public class TransactionServerImpl extends UnicastRemoteObject implements TransactionServer,
                                                                          RemoteServer,
                                                                          StatsCollector
{

    public static final int SESSION_TIMEOUT_MS = 240000; // 240 seconds

    private static Category debugLog = Logger.getInstance("XServDebug");
    private static Category perfLog = Logger.getInstance("XServPerf");

    private String serverID;
    private Hashtable sessions = new Hashtable();
    private Hashtable sessionsByUserName = new Hashtable();
    private int sessionIdSpool = 0;
    private AuditLog audit;
    private ServerFactory factory = new ServerFactory();
    private SessionSweepThread sessionSweepThread = new SessionSweepThread();
    private Stats myStats = new Stats(Stats.TRANSACTION, "TransactionServer");
    private int _port;

    private TriggerEngine triggerEngine = new TriggerEngine();
    private int transactionId = 0;

    private StatsLoggerThread statsLoggerThread;

    public static final String RMI_NAME = "G2TransactionServer";

    public TransactionServerImpl() throws RemoteException, CommException {
        super();
        debugLog.info("Constructor.  Server ID: " + serverID);
        this.serverID = "TransactionServer";

        this.audit = factory.getAuditServer();

        new Thread(sessionSweepThread).start();
        statsLoggerThread = new StatsLoggerThread();
    }

    //
    // RemoteServer interface
    //
    public void serverStart(int port) throws RemoteException, CommException {
        try {
            debugLog.info("Startup on port " + Integer.toString(port));
            ServerFactory.serverStart(this, port, RMI_NAME);
            _port = port;
            statsLoggerThread.start();
        } catch (Exception err) {
            err.printStackTrace();
            throw new CommException(err.getMessage());
        }
    }

    public void serverStop() throws RemoteException, CommException {
        try {
            debugLog.info("Shutdown signal received.");
            sessionSweepThread.terminate();
            try {
                debugLog.info("Terminating Session Sweep Thread");
                sessionSweepThread.join();
            } catch (InterruptedException ignored) {
            }

            statsLoggerThread.terminate();
            try {
                debugLog.info("Terminating Stats Logger Thread");
                statsLoggerThread.join();
            } catch (InterruptedException ignored) {
            }
            debugLog.info("Shutdown complete.");
        } catch (Exception err) {
            err.printStackTrace();
            throw new CommException(err.getMessage());
        }
    }

    public void setServerName(String name) throws RemoteException {
        serverID = name;
        myStats.name = name;
    }

    public String getServerName() throws RemoteException {
        return serverID;
    }

    public String getHostName() throws RemoteException {
        try {
            return InetAddress.getLocalHost().getHostName();
        } catch (UnknownHostException err) {
            return null;
        }

    }


    /**
     * Opens a new session, given a user name and password.
     *
     * @return A session ID the corresponds to the new session.
     * @throws SessionExistsException
     *         LoginException
     *         CommException
     */
    public long openSession(String username, String password)
        throws SessionExistsException,
               LoginException,
               CommException,
               RemoteException
    {
        if (username == null || password == null) {
            debugLog.info("openSession(): Null username or password");
            throw new IllegalArgumentException("Null parameter is invalid.");
        }

        debugLog.debug("openSession: " + username + ": " + password);

        if (username.length() == 0) {
            throw new LoginException("No Username Specified.");
        }

        synchronized (sessions) {
            if (this.sessionsByUserName.get(username) != null) {
                debugLog.info("openSession(): session already exists for " + username);
                throw new SessionExistsException(username);
            }
        }

        // Authenticate
        // int userId = authServer.login(username, password);
        debugLog.debug("openSession: performing DB login");
        DBServer dbServer = this.factory.getDBServer();
        int userId = dbServer.login(username);
        debugLog.debug("openSession: Getting DB user info");
        UserInfo userInfo = dbServer.getUserInfo(userId);

        Session newSession = new Session(userId, userInfo, this, newSessionId());
        synchronized (sessions) {
            this.sessions.put(newSession.sessionId, newSession);
            this.sessionsByUserName.put(username, newSession);
        }

        myStats.addSession();

        long id = newSession.sessionId.longValue();
        debugLog.info("Session opened for user " + username + ", id " + Long.toString(id));

        try {
            //
            // Register session with LoadBalancer/ServerRegistry
            //
            ServerManager serverManager = (ServerManager)factory.getServerRegistry();
            serverManager.registerSession(id, serverID);
        } catch (RemoteException err) {
            debugLog.fatal("Failed to register session: RemoteException", err);
            throw err;
        } catch (SessionNotFoundException err) {
            debugLog.fatal("Failed to register session: Server not registered", err);
            throw new CommException(err.getMessage());
        }

        return id;
    }

    public void pingSession(long sessionId) throws SessionNotFoundException,
                                                   RemoteException
    {
        debugLog.debug("pingSession: " + Long.toString(sessionId));
        getSessionById(sessionId).ping();
    }

    public void closeSession(long sessionId) throws SessionNotFoundException,
                                                    RemoteException
    {
        debugLog.debug("closeSession(): " + Long.toString(sessionId));
        synchronized (sessions) {
            removeSession(getSessionById(sessionId));
            myStats.removeSession();
        }
    }

    public UserInfo getUserInfo(int transId, long sessionId) throws SessionNotFoundException,
                                                                    RemoteException
    {
        final String CMDNAME = "DISPLAY_SUMMARY";

        long startTime = System.currentTimeMillis();

        debugLog.debug("getUserInfo() session: " + Long.toString(sessionId));
        Session session = getSessionById(sessionId);
        synchronized (session) {
            try {
                audit.systemDisplaySummary(serverID, transId, session.userInfo.username);

                UserInfo userInfo = new UserInfo(session.userInfo);
                for (Iterator i = session.buyCommands.iterator(); i.hasNext(); ) {
                    userInfo.pendingBuys.add(i.next());
                }

                for (Iterator i = session.sellCommands.iterator(); i.hasNext(); ) {
                    userInfo.pendingSells.add(i.next());
                }

                if (debugLog.isDebugEnabled()) {
                    debugLog.debug("getUserInfo: numStocks=" + Integer.toString(userInfo.stocks.size()));
                    debugLog.debug("getUserInfo: pendingBuys=" + Integer.toString(userInfo.pendingBuys.size()));
                    debugLog.debug("getUserInfo: pendingSells=" + Integer.toString(userInfo.pendingSells.size()));
                }

                addTransactionStats(startTime);

                return userInfo;
            } catch (RuntimeException err) {
                audit.errorEvent(serverID, transId, session.userInfo.username, CMDNAME, "RuntimeException: " + err.getMessage());
                debugLog.error("getUserInfo", err);
                myStats.addError();
                throw err;
            } catch (RemoteException err) {
                audit.errorEvent(serverID, transId, session.userInfo.username, CMDNAME, "RemoteException: " + err.getMessage());
                debugLog.error("getUserInfo", err);
                myStats.addError();
                throw err;
            }
        }
    }

    private void addTransactionStats(long startTime) {
        myStats.addTransaction(startTime);
        if (perfLog.isDebugEnabled()) {
            long currentTime = System.currentTimeMillis();
            perfLog.debug("Transaction Time: " + (currentTime - startTime));
        }
    }


    public String getLogFile(int transId, long sessionId, boolean forUser) throws SessionNotFoundException, RemoteException, IOException {
        final String CMDNAME = "DUMPLOG";

        debugLog.debug("getLogFile() session: " + Long.toString(sessionId));

        long startTime = System.currentTimeMillis();

        String username = null;
        if (forUser) {
            username = getSessionById(sessionId).userInfo.username;
        }

        try {
            String filename = audit.dumpLog(username);
            debugLog.debug("Created log file at " + filename);
            audit.systemDisplayLog(serverID, transId, username, filename);
            addTransactionStats(startTime);
            return filename;

        } catch (RuntimeException err) {
            audit.errorEvent(serverID, transId, null, CMDNAME, "RuntimeException: " + err.getMessage());
            debugLog.error("getLogFile", err);
            myStats.addError();
            throw err;
        } catch (RemoteException err) {
            audit.errorEvent(serverID, transId, null, CMDNAME, "RemoteException: " + err.getMessage());
            debugLog.error("getLogFile", err);
            myStats.addError();
            throw err;
        } catch (IOException err) {
            audit.errorEvent(serverID, transId, null, CMDNAME, "IOException: " + err.getMessage());
            debugLog.error("getLogFile", err);
            myStats.addError();
            throw err;
        }
    }

    public Quote quote(int transId, long sessionId, String stock) throws SessionNotFoundException,
                                                                         CommException,
                                                                         RemoteException
    {
        debugLog.debug("quote: session: " + Long.toString(sessionId));

        long startTime = System.currentTimeMillis();

        final String CMDNAME = "QUOTE";
        Session session = getSessionById(sessionId);

        try {

            audit.systemQuote(serverID, transId, session.userInfo.username, stock);

            if (stock == null || stock.length() == 0) {
                throw new CommException("Missing stock symbol");
            }

            debugLog.debug("quote: stock: " + stock);
            if (perfLog.isInfoEnabled()) {
                perfLog.info(Long.toString(sessionId) + ": Begin quote");
            }
            Quote quote = this.factory.getQuoteServer().quote(transId, stock, session.userInfo.username);
            if (perfLog.isInfoEnabled()) {
                perfLog.info(Long.toString(sessionId) + ": End quote");
            }

            addTransactionStats(startTime);
            return quote;

        } catch (RuntimeException err) {
            audit.errorEvent(serverID, transId, session.userInfo.username, CMDNAME, "RuntimeException: " + err.getMessage());
            debugLog.error("quote", err);
            myStats.addError();
            throw err;
        } catch (CommException err) {
            audit.errorEvent(serverID, transId, session.userInfo.username, CMDNAME, "CommException: " + err.getMessage());
            debugLog.warn("quote: " + err.getMessage());
            myStats.addError();
            throw err;
        } catch (RemoteException err) {
            audit.errorEvent(serverID, transId, session.userInfo.username, CMDNAME, "RemoteException: " + err.getMessage());
            debugLog.error("quote", err);
            myStats.addError();
            throw err;
        }
    }

    public PurchaseOrder buy(int transId, long sessionId, String stock, float dollarAmount)
        throws SessionNotFoundException,
               CommException,
               InvalidCommandException,
               RemoteException
    {
        debugLog.debug("buy: session: " + Long.toString(sessionId));

        long startTime = System.currentTimeMillis();


        final String CMDNAME = "BUY";
        Session session = getSessionById(sessionId);

        try {
            synchronized (session) {
                audit.systemBuy(serverID, transId, session.userInfo.username, stock, dollarAmount);
                session.sweepBuyCommands();

                if (stock == null || stock.length() == 0) {
                    throw new CommException("Missing stock symbol.");
                }

                debugLog.debug("buy: stock: " + stock + " amount: " + Float.toString(dollarAmount));

                // Ensure funds available
                if (dollarAmount > session.userInfo.balance.doubleValue()) {
                    throw new InsufficientFundsException(session.userInfo.balance.doubleValue());
                }

                // Ensure user can buy at least one share
                if (perfLog.isInfoEnabled()) {
                    perfLog.info(Long.toString(sessionId) + ": Begin quote");
                }
                Quote quote = this.factory.getQuoteServer().quote(transId, stock, session.userInfo.username);
                if (perfLog.isInfoEnabled()) {
                    perfLog.info(Long.toString(sessionId) + ": End quote");
                }
                if (quote.price.doubleValue() > dollarAmount) {
                    throw new InsufficientFundsException(quote.price.doubleValue(), "Minimum stock price");
                }

                // Good to go.  Create order
                PurchaseOrder order = new PurchaseOrder(PurchaseOrder.BUY);
                order.quote = quote;
                order.expiry = quote.expiry;
                order.amount = (int)Math.floor(dollarAmount / quote.price.doubleValue());
                order.total = new Money(quote.price.doubleValue() * order.amount);

                session.addBuyCommand(order);

                addTransactionStats(startTime);

                return order;
            }

        } catch (RuntimeException err) {
            audit.errorEvent(serverID, transId, session.userInfo.username, CMDNAME, "RuntimeException: " + err.getMessage());
            debugLog.error("buy", err);
            myStats.addError();
            throw err;
        } catch (InvalidCommandException err) {
            audit.errorEvent(serverID, transId, session.userInfo.username, CMDNAME, "InvalidCommandException: " + err.getMessage());
            debugLog.warn("buy " + err.getMessage());
            myStats.addError();
            throw err;
        } catch (CommException err) {
            audit.errorEvent(serverID, transId, session.userInfo.username, CMDNAME, "CommException: " + err.getMessage());
            debugLog.warn("buy " + err.getMessage());
            myStats.addError();
            throw err;
        } catch (RemoteException err) {
            audit.errorEvent(serverID, transId, session.userInfo.username, CMDNAME, "RemoteException: " + err.getMessage());
            debugLog.error("buy", err);
            myStats.addError();
            throw err;
        }
    }


    public Receipt commitBuy(int transId, long sessionId) throws SessionNotFoundException,
                                                                 CommException,
                                                                 NoPendingCommandException,
                                                                 RemoteException
    {
        debugLog.debug("TS.commitBuy: session: " + Long.toString(sessionId));

        long startTime = System.currentTimeMillis();

        final String CMDNAME = "COMMIT_BUY";
        Session session = getSessionById(sessionId);

        try {
            synchronized (session) {
                audit.systemCommitBuy(serverID, transId, session.userInfo.username);

                session.sweepBuyCommands();

                if (session.buyCommands.isEmpty()) {
                    throw new NoPendingCommandException("Buy");
                }

                // Remove oldest command
                PurchaseOrder order = (PurchaseOrder)session.buyCommands.get(0);

                // Send to database
                Receipt receipt = this.factory.getDBServer().buy(transId, session.userId, order);

                // Success - remove command and adjust balance
                session.buyCommands.remove(0);
                session.userInfo.reserveBalance = session.userInfo.reserveBalance.subtract(order.total);
                StockRecord stock = (StockRecord)session.userInfo.stocks.get(receipt.order.quote.stock);
                if (stock == null) {
                    stock = new StockRecord(receipt.order.quote.stock, receipt.order.amount);
                    session.userInfo.stocks.put(stock.stock, stock);
                } else {
                    stock.amount += receipt.order.amount;
                }

                addTransactionStats(startTime);
                return receipt;
            }
        } catch (RuntimeException err) {
            audit.errorEvent(serverID, transId, session.userInfo.username, CMDNAME, "RuntimeException: " + err.getMessage());
            debugLog.error(CMDNAME, err);
            myStats.addError();
            throw err;
        } catch (CommException err) {
            audit.errorEvent(serverID, transId, session.userInfo.username, CMDNAME, "CommException: " + err.getMessage());
            debugLog.warn(CMDNAME + err.getMessage());
            myStats.addError();
            throw err;
        } catch (NoPendingCommandException err) {
            audit.errorEvent(serverID, transId, session.userInfo.username, CMDNAME, "NoPendingCommandException: " + err.getMessage());
            debugLog.warn(CMDNAME + err.getMessage());
            myStats.addError();
            throw err;
        } catch (RemoteException err) {
            audit.errorEvent(serverID, transId, session.userInfo.username, CMDNAME, "RemoteException: " + err.getMessage());
            debugLog.error(CMDNAME, err);
            myStats.addError();
            throw err;
        }
    }

    public PurchaseOrder cancelBuy(int transId, long sessionId) throws SessionNotFoundException,
                                                                       CommException,
                                                                       NoPendingCommandException,
                                                                       RemoteException
    {
        long startTime = System.currentTimeMillis();

        debugLog.debug("TS.cancelBuy: session: " + Long.toString(sessionId));

        final String CMDNAME = "CANCEL_BUY";
        Session session = getSessionById(sessionId);

        try {
            synchronized (session) {
                audit.systemCancelBuy(serverID, transId, session.userInfo.username);
                addTransactionStats(startTime);
                return session.cancelBuy();
            }
        } catch (RuntimeException err) {
            audit.errorEvent(serverID, transId, session.userInfo.username, CMDNAME, "RuntimeException: " + err.getMessage());
            debugLog.error(CMDNAME, err);
            myStats.addError();
            throw err;
//         } catch (CommException err) {
//             audit.errorEvent(serverID, transId, session.userInfo.username, CMDNAME, "CommException: " + err.getMessage());
//             throw err;
        } catch (NoPendingCommandException err) {
            audit.errorEvent(serverID, transId, session.userInfo.username, CMDNAME, "NoPendingCommandException: " + err.getMessage());
            debugLog.info(CMDNAME + err.getMessage());
            myStats.addError();
            throw err;
        } catch (RemoteException err) {
            audit.errorEvent(serverID, transId, session.userInfo.username, CMDNAME, "RemoteException: " + err.getMessage());
            debugLog.error(CMDNAME, err);
            myStats.addError();
            throw err;
        }
    }

    public Receipt addFunds(int transId, long sessionId, float amount) throws SessionNotFoundException,
                                                                              CommException,
                                                                              InvalidCommandException,
                                                                              RemoteException
    {
        long startTime = System.currentTimeMillis();
        debugLog.debug("TS.addFunds: session: " + Long.toString(sessionId) + " amount: " + Float.toString(amount));

        final String CMDNAME = "ADD";
        Session session = getSessionById(sessionId);

        try {
            synchronized (session) {
                audit.systemAddFunds(serverID, transId, session.userInfo.username, amount);

                if (amount <= 0.0f) {
                    String errorMessage = "Can't add zero or negative funds.";
                    throw new InvalidCommandException(errorMessage);
                }
                Receipt r = this.factory.getDBServer().addFunds(transId, session.userId, amount);
                session.userInfo.balance = r.balance;
                addTransactionStats(startTime);
                return r;
            }
        } catch (RuntimeException err) {
            audit.errorEvent(serverID, transId, session.userInfo.username, CMDNAME, "RuntimeException: " + err.getMessage());
            debugLog.error(CMDNAME, err);
            throw err;
        } catch (CommException err) {
            audit.errorEvent(serverID, transId, session.userInfo.username, CMDNAME, "CommException: " + err.getMessage());
            debugLog.info(CMDNAME + err.getMessage());
            throw err;
        } catch (InvalidCommandException err) {
            audit.errorEvent(serverID, transId, session.userInfo.username, CMDNAME, "InvalidCommandException: " + err.getMessage());
            debugLog.info(CMDNAME + err.getMessage());
            throw err;
        } catch (RemoteException err) {
            audit.errorEvent(serverID, transId, session.userInfo.username, CMDNAME, "RemoteException: " + err.getMessage());
            debugLog.error(CMDNAME, err);
            throw err;
        }
    }



    public PurchaseOrder sell(int transId, long sessionId, String stock, float amount)
        throws SessionNotFoundException,
               CommException,
               InvalidCommandException,
               RemoteException
    {
        debugLog.debug("TS.sell: session: " + Long.toString(sessionId) + " amount: " + amount);
        long startTime = System.currentTimeMillis();

        final String CMDNAME = "SELL";
        Session session = getSessionById(sessionId);

        try {
            synchronized (session) {
                audit.systemSell(serverID, transId, session.userInfo.username, stock, amount);

                if (stock == null || stock.length() == 0) {
                    throw new InvalidCommandException("Missing stock symbol.");
                }

                debugLog.debug("TS.sell: stock: " + stock);

                if (amount <= 0.0f) {
                    throw new InvalidCommandException("Cannot sell less than 0 stocks");
                }

                stock = stock.toUpperCase();
                StockRecord record = (StockRecord)session.userInfo.stocks.get(stock);
                if (record == null || record.amount <= 0) {
                    throw new InvalidCommandException("You do not own any stock of '" + stock + "'");
                }

                Quote q = this.factory.getQuoteServer().quote(transId, stock, session.userInfo.username);
                if (record.amount * q.price.doubleValue() < amount) {
                    throw new InvalidCommandException("You cannot sell $" + amount + ", which is more than you own: $" +
                                                      (record.amount * q.price.doubleValue()));
                }

                int numStocks = (int)(amount / q.price.doubleValue());
                // round up
                if (amount % q.price.doubleValue() > 0) {
                    ++numStocks;
                }

                if (record.amount < numStocks) {
                    //throw new InsufficientFundsException(record.amount, "Maximum number to sell");
                    numStocks = record.amount;
                }

                // Good to go.  Create order
                PurchaseOrder order = new PurchaseOrder(PurchaseOrder.SELL);
                order.quote = q;
                order.expiry = q.expiry;
                order.amount = numStocks;
                order.total = new Money(q.price.doubleValue() * order.amount);

                session.addSellCommand(order, record);
                addTransactionStats(startTime);
                return order;
            }

        } catch (RuntimeException err) {
            audit.errorEvent(serverID, transId, session.userInfo.username, CMDNAME, "RuntimeException: " + err.getMessage());
            debugLog.error(CMDNAME, err);
            myStats.addError();
            throw err;
        } catch (CommException err) {
            audit.errorEvent(serverID, transId, session.userInfo.username, CMDNAME, "CommException: " + err.getMessage());
            debugLog.info(CMDNAME + err.getMessage());
            myStats.addError();
            throw err;
        } catch (InvalidCommandException err) {
            audit.errorEvent(serverID, transId, session.userInfo.username, CMDNAME, "InvalidCommandException: " + err.getMessage());
            debugLog.info(CMDNAME + err.getMessage());
            myStats.addError();
            throw err;
        } catch (RemoteException err) {
            audit.errorEvent(serverID, transId, session.userInfo.username, CMDNAME, "RemoteException: " + err.getMessage());
            debugLog.error(CMDNAME, err);
            myStats.addError();
            throw err;
        }
    }


    public Receipt commitSell(int transId, long sessionId) throws SessionNotFoundException,
                                                                  CommException,
                                                                  NoPendingCommandException,
                                                                  RemoteException
    {
        long startTime = System.currentTimeMillis();
        debugLog.debug("TS.commitSell: session: " + Long.toString(sessionId));

        final String CMDNAME = "COMMIT_SELL";
        Session session = getSessionById(sessionId);

        try {
            synchronized (session) {
                audit.systemCommitSell(serverID, transId, session.userInfo.username);

                session.sweepSellCommands();

                if (session.sellCommands.isEmpty()) {
                    throw new NoPendingCommandException("Sell");
                }

                // Remove oldest command
                PurchaseOrder order = (PurchaseOrder)session.sellCommands.get(0);

                // Send to database
                Receipt receipt = this.factory.getDBServer().sell(transId, session.userId, order);

                // Success - remove command and adjust balance
                session.sellCommands.remove(0);
                session.userInfo.balance = session.userInfo.balance.add(order.total);
                StockRecord stock = (StockRecord)session.userInfo.stocks.get(receipt.order.quote.stock);
                if (stock == null) {
                    // !!! Error - log this
                    debugLog.error("TransactionServer.commitSell() Error: no stock record");
                } else {
                    stock.reserveAmount -= receipt.order.amount;
                    if (stock.reserveAmount < 0) {
                        // !!! Error - log this
                        debugLog.error("TransactionServer.commitSell() Error: negative reserve amount");
                        stock.reserveAmount = 0;
                    }
                    if (stock.amount == 0) {
                        // Sold all stock.  Remove it from the list
                        session.userInfo.stocks.remove(stock.stock);
                    }
                }
                addTransactionStats(startTime);
                return receipt;
            }
        } catch (RuntimeException err) {
            audit.errorEvent(serverID, transId, session.userInfo.username, CMDNAME, "RuntimeException: " + err.getMessage());
            debugLog.error(CMDNAME, err);
            myStats.addError();
            throw err;
        } catch (CommException err) {
            audit.errorEvent(serverID, transId, session.userInfo.username, CMDNAME, "CommException: " + err.getMessage());
            debugLog.warn(CMDNAME + err.getMessage());
            myStats.addError();
            throw err;
        } catch (NoPendingCommandException err) {
            audit.errorEvent(serverID, transId, session.userInfo.username, CMDNAME, "NoPendingCommandException: " + err.getMessage());
            debugLog.warn(CMDNAME + err.getMessage());
            myStats.addError();
            throw err;
        } catch (RemoteException err) {
            audit.errorEvent(serverID, transId, session.userInfo.username, CMDNAME, "RemoteException: " + err.getMessage());
            debugLog.error(CMDNAME, err);
            myStats.addError();
            throw err;
        }
     }

    public PurchaseOrder cancelSell(int transId, long sessionId) throws SessionNotFoundException,
                                                                        CommException,
                                                                        NoPendingCommandException,
                                                                        RemoteException
    {
        long startTime = System.currentTimeMillis();
        debugLog.debug("TS.cancelSell: session: " + Long.toString(sessionId));

        final String CMDNAME = "CANCEL_SELL";
        Session session = getSessionById(sessionId);

        try {
            synchronized (session) {
                audit.systemCancelSell(serverID, transId, session.userInfo.username);
                addTransactionStats(startTime);
                return session.cancelSell();
            }
        } catch (RuntimeException err) {
            audit.errorEvent(serverID, transId, session.userInfo.username, CMDNAME, "RuntimeException: " + err.getMessage());
            debugLog.error(CMDNAME, err);
            myStats.addError();
            throw err;
//         } catch (CommException err) {
//             audit.errorEvent(serverID, transId, session.userInfo.username, CMDNAME, "CommException: " + err.getMessage());
//             throw err;
        } catch (NoPendingCommandException err) {
            audit.errorEvent(serverID, transId, session.userInfo.username, CMDNAME, "NoPendingCommandException: " + err.getMessage());
            debugLog.warn(CMDNAME + err.getMessage());
            myStats.addError();
            throw err;
        } catch (RemoteException err) {
            audit.errorEvent(serverID, transId, session.userInfo.username, CMDNAME, "RemoteException: " + err.getMessage());
            debugLog.error(CMDNAME, err);
            myStats.addError();
            throw err;
        }
    }



    public Trigger createBuyTrigger(int transId, long sessionId, String stock, float amount) throws RemoteException,
                                                                                                    CommException,
                                                                                                    InvalidCommandException,
                                                                                                    SessionNotFoundException
    {
        long startTime = System.currentTimeMillis();
        debugLog.debug("TS.createBuyTrigger: session: " + Long.toString(sessionId));

        final String CMDNAME = "SET_BUY_AMOUNT";
        Session session = getSessionById(sessionId);

        try {
            synchronized (session) {
                audit.systemNewBuyTrigger(serverID, transId, session.userInfo.username, stock, amount);

                if (stock == null || stock.length() == 0) {
                    throw new InvalidCommandException("Missing stock symbol.");
                }
                stock = stock.toUpperCase();

                debugLog.debug("TS.createBuyTrigger: stock: " + stock + " amount: " + Float.toString(amount));

                if (amount <= 0.0) {
                    throw new InvalidCommandException("Negative purchase amount.");
                }

                // If there is another pending buy for this stock, remove it first
                session.removePendingBuyTrigger(stock);

                if (amount > session.userInfo.balance.doubleValue()) {
                    throw new InsufficientFundsException(session.userInfo.balance.doubleValue());
                }

                Trigger trigger = new Trigger(Trigger.BUY);
                trigger.stock = stock;
                trigger.amount = amount;

                session.addPendingBuyTrigger(trigger);
                addTransactionStats(startTime);
                return trigger;
            }
        } catch (RuntimeException err) {
            audit.errorEvent(serverID, transId, session.userInfo.username, CMDNAME, "RuntimeException: " + err.getMessage());
            debugLog.error(CMDNAME, err);
            myStats.addError();
            throw err;
//         } catch (CommException err) {
//             audit.errorEvent(serverID, transId, session.userInfo.username, CMDNAME, "CommException: " + err.getMessage());
//             throw err;
        } catch (InvalidCommandException err) {
            audit.errorEvent(serverID, transId, session.userInfo.username, CMDNAME, "InvalidCommandException: " + err.getMessage());
            debugLog.warn(CMDNAME + err.getMessage());
            myStats.addError();
            throw err;
        } catch (RemoteException err) {
            audit.errorEvent(serverID, transId, session.userInfo.username, CMDNAME, "RemoteException: " + err.getMessage());
            debugLog.error(CMDNAME, err);
            myStats.addError();
            throw err;
        }
    }

    public void commitBuyTrigger(int transId, long sessionId, String stock, float triggerPrice)
        throws RemoteException,
               CommException,
               InvalidCommandException,
               SessionNotFoundException
    {
        long startTime = System.currentTimeMillis();
        debugLog.debug("TS.createBuyTrigger: session: " + Long.toString(sessionId));

        final String CMDNAME = "SET_BUY_TRIGGER";
        Session session = getSessionById(sessionId);

        try {
            synchronized (session) {
                audit.systemCommitBuyTrigger(serverID, transId, session.userInfo.username, stock, triggerPrice);

                if (stock == null || stock.length() == 0) {
                    throw new InvalidCommandException("Missing stock symbol.");
                }
                stock = stock.toUpperCase();
                debugLog.debug("TS.commitBuyTrigger: stock: " + stock + " price: " + Float.toString(triggerPrice));

                if (triggerPrice <= 0.0) {
                    throw new InvalidCommandException("Negative trigger amount.");
                }

                Trigger trigger = (Trigger)session.pendingBuyTriggers.get(stock);
                if (trigger == null) {
                    throw new InvalidCommandException("No pending trigger for stock '" + stock + "'");
                }

                if (triggerPrice > trigger.amount) {
                    throw new InvalidCommandException("Trigger price cannot be more than " + Float.toString(trigger.amount));
                }

                // Add trigger to database
                trigger.stockPrice = triggerPrice;
                trigger.kind = Trigger.BUY;
                int triggerId = this.factory.getDBServer().addBuyTrigger(session.userId, trigger);

                // Finialize trigger properties
                trigger.id = triggerId;
                trigger.username = session.userInfo.username;

                // Remove the pending trigger, and add a real trigger
                session.removePendingBuyTrigger(stock);
                session.userInfo.balance = session.userInfo.balance.subtract(trigger.amount);
                session.userInfo.triggerReserveBalance = session.userInfo.triggerReserveBalance.add(trigger.amount);
                triggerEngine.addBuyTrigger(trigger);
                session.userInfo.buyTriggers.put(trigger.stock, trigger);
                addTransactionStats(startTime);
            }
        } catch (RuntimeException err) {
            audit.errorEvent(serverID, transId, session.userInfo.username, CMDNAME, "RuntimeException: " + err.getMessage());
            debugLog.error(CMDNAME, err);
            myStats.addError();
            throw err;
        } catch (CommException err) {
            audit.errorEvent(serverID, transId, session.userInfo.username, CMDNAME, "CommException: " + err.getMessage());
            debugLog.warn(CMDNAME + err.getMessage());
            myStats.addError();
            throw err;
        } catch (InvalidCommandException err) {
            audit.errorEvent(serverID, transId, session.userInfo.username, CMDNAME, "InvalidCommandException: " + err.getMessage());
            debugLog.warn(CMDNAME + err.getMessage());
            myStats.addError();
            throw err;
        } catch (RemoteException err) {
            audit.errorEvent(serverID, transId, session.userInfo.username, CMDNAME, "RemoteException: " + err.getMessage());
            debugLog.error(CMDNAME, err);
            myStats.addError();
            throw err;
        }
    }

    public void cancelBuyTrigger(int transId, long sessionId, String stock) throws RemoteException,
                                                                                   CommException,
                                                                                   InvalidCommandException,
                                                                                   SessionNotFoundException
    {
        long startTime = System.currentTimeMillis();
        debugLog.debug("TS.cancelBuyTrigger: session: " + Long.toString(sessionId));

        final String CMDNAME = "CANCEL_SET_BUY";
        Session session = getSessionById(sessionId);

        try {
            synchronized (session) {
                audit.systemCancelBuyTrigger(serverID, transId, session.userInfo.username, stock);

                if (stock == null || stock.length() == 0) {
                    throw new InvalidCommandException("Missing stock symbol.");
                }
                stock = stock.toUpperCase();
                debugLog.debug("TS.cancelBuyTrigger: stock: " + stock);

                // Cancel any pending trigger
                session.removePendingBuyTrigger(stock);

                Trigger trigger = triggerEngine.findBuyTrigger(session.userInfo.username, stock);
                if (trigger != null) {
                    // Cancel real trigger
                    debugLog.debug("TS: cancelling trigger from database...");
                    triggerEngine.removeBuyTrigger(trigger);
                    this.factory.getDBServer().cancelBuyTrigger(session.userId, trigger.id);
                    session.userInfo.balance = session.userInfo.balance.add(trigger.amount);
                    session.userInfo.triggerReserveBalance = session.userInfo.triggerReserveBalance.subtract(trigger.amount);

                }
                session.userInfo.buyTriggers.remove(stock);
                addTransactionStats(startTime);
            }
        } catch (RuntimeException err) {
            audit.errorEvent(serverID, transId, session.userInfo.username, CMDNAME, "RuntimeException: " + err.getMessage());
            debugLog.error(CMDNAME, err);
            myStats.addError();
            throw err;
        } catch (CommException err) {
            audit.errorEvent(serverID, transId, session.userInfo.username, CMDNAME, "CommException: " + err.getMessage());
            debugLog.warn(CMDNAME + err.getMessage());
            myStats.addError();
            throw err;
        } catch (InvalidCommandException err) {
            audit.errorEvent(serverID, transId, session.userInfo.username, CMDNAME, "InvalidCommandException: " + err.getMessage());
            debugLog.warn(CMDNAME + err.getMessage());
            myStats.addError();
            throw err;
        } catch (RemoteException err) {
            audit.errorEvent(serverID, transId, session.userInfo.username, CMDNAME, "RemoteException: " + err.getMessage());
            debugLog.error(CMDNAME, err);
            myStats.addError();
            throw err;
        }
    }

    public Trigger createSellTrigger(int transId, long sessionId, String stock, int amount) throws RemoteException,
                                                                                                   CommException,
                                                                                                   InvalidCommandException,
                                                                                                   SessionNotFoundException
    {
        long startTime = System.currentTimeMillis();
        debugLog.debug("TS.createSellTrigger: session: " + Long.toString(sessionId));

        final String CMDNAME = "SET_SELL_AMOUNT";
        Session session = getSessionById(sessionId);

        try {
            synchronized (session) {
                audit.systemNewSellTrigger(serverID, transId, session.userInfo.username, stock, amount);

                if (stock == null || stock.length() == 0) {
                    throw new InvalidCommandException("Missing stock symbol.");
                }
                stock = stock.toUpperCase();
                debugLog.debug("TS.createSellTrigger: stock: " + stock + " amount: " + Integer.toString(amount));


                if (amount <= 0) {
                    throw new InvalidCommandException("Negative sell amount.");
                }

                // If there is another pending buy for this stock, remove it first
                session.removePendingSellTrigger(stock);

                StockRecord record = (StockRecord)session.userInfo.stocks.get(stock);
                if (record == null) {
                    throw new InvalidCommandException("You do not own '" + stock + "'");
                }

                if (record.amount < amount) {
                    throw new InsufficientFundsException(record.amount, "Attempt to sell more than is owned");
                }

                if (amount > session.userInfo.balance.doubleValue()) {
                    throw new InsufficientFundsException(session.userInfo.balance.doubleValue());
                }

                Trigger trigger = new Trigger(Trigger.SELL);
                trigger.stock = stock;
                trigger.amount = (float)amount;
                TriggerWrapper wrapper = new TriggerWrapper(trigger, record);

                session.addPendingSellTrigger(wrapper);
                addTransactionStats(startTime);
                return new Trigger(trigger);
            }

        } catch (RuntimeException err) {
            audit.errorEvent(serverID, transId, session.userInfo.username, CMDNAME, "RuntimeException: " + err.getMessage());
            debugLog.error(CMDNAME, err);
            myStats.addError();
            throw err;
//         } catch (CommException err) {
//             audit.errorEvent(serverID, transId, session.userInfo.username, CMDNAME, "CommException: " + err.getMessage());
//             throw err;
        } catch (InvalidCommandException err) {
            audit.errorEvent(serverID, transId, session.userInfo.username, CMDNAME, "InvalidCommandException: " + err.getMessage());
            debugLog.warn(CMDNAME + err.getMessage());
            myStats.addError();
            throw err;
        } catch (RemoteException err) {
            audit.errorEvent(serverID, transId, session.userInfo.username, CMDNAME, "RemoteException: " + err.getMessage());
            debugLog.error(CMDNAME, err);
            myStats.addError();
            throw err;
        }
    }

    public void commitSellTrigger(int transId, long sessionId, String stock, float triggerPrice) throws RemoteException,
                                                                                                        CommException,
                                                                                                        InvalidCommandException,
                                                                                                        SessionNotFoundException
    {
        long startTime = System.currentTimeMillis();
        debugLog.debug("TS.commitSellTrigger: session: " + Long.toString(sessionId));

        final String CMDNAME = "SET_SELL_TRIGGER";
        Session session = getSessionById(sessionId);

        try {
            synchronized (session) {
                audit.systemCommitSellTrigger(serverID, transId, session.userInfo.username, stock, triggerPrice);

                if (stock == null || stock.length() == 0) {
                    throw new InvalidCommandException("Missing stock symbol.");
                }
                stock = stock.toUpperCase();
                debugLog.debug("TS.commitSellTrigger: stock: " + stock + " price: " + Float.toString(triggerPrice));

                if (triggerPrice <= 0.0) {
                    throw new InvalidCommandException("Negative trigger amount.");
                }

                TriggerWrapper wrapper = (TriggerWrapper)session.pendingSellTriggers.get(stock);
                if (wrapper == null) {
                    throw new InvalidCommandException("No pending trigger for stock '" + stock + "'");
                }

                // Add trigger to database
                wrapper.trigger.stockPrice = triggerPrice;
                int triggerId = this.factory.getDBServer().addSellTrigger(session.userId, wrapper.trigger);

                // Finialize trigger properties
                wrapper.trigger.id = triggerId;
                wrapper.trigger.username = session.userInfo.username;

                // Remove the pending trigger, and add a real trigger
                session.removePendingSellTrigger(stock);

                wrapper.stockRecord.amount -= wrapper.trigger.amount;
                wrapper.stockRecord.triggerReserveAmount += wrapper.trigger.amount;

                triggerEngine.addSellTrigger(wrapper.trigger);

                session.userInfo.sellTriggers.put(wrapper.trigger.stock, wrapper.trigger);
                addTransactionStats(startTime);
            }
        } catch (RuntimeException err) {
            audit.errorEvent(serverID, transId, session.userInfo.username, CMDNAME, "RuntimeException: " + err.getMessage());
            debugLog.error(CMDNAME, err);
            myStats.addError();
            throw err;
        } catch (CommException err) {
            audit.errorEvent(serverID, transId, session.userInfo.username, CMDNAME, "CommException: " + err.getMessage());
            debugLog.warn(CMDNAME + err.getMessage());
            myStats.addError();
            throw err;
        } catch (InvalidCommandException err) {
            audit.errorEvent(serverID, transId, session.userInfo.username, CMDNAME, "InvalidCommandException: " + err.getMessage());
            debugLog.warn(CMDNAME + err.getMessage());
            myStats.addError();
            throw err;
        } catch (RemoteException err) {
            audit.errorEvent(serverID, transId, session.userInfo.username, CMDNAME, "RemoteException: " + err.getMessage());
            debugLog.error(CMDNAME, err);
            myStats.addError();
            throw err;
        }
    }

    public void cancelSellTrigger(int transId, long sessionId, String stock) throws RemoteException,
                                                                                    CommException,
                                                                                    InvalidCommandException,
                                                                                    SessionNotFoundException
    {
        long startTime = System.currentTimeMillis();
        debugLog.debug("TS.cancelSellTrigger: session: " + Long.toString(sessionId));

        final String CMDNAME = "CANCEL_SET_SELL";
        Session session = getSessionById(sessionId);

        try {
            synchronized (session) {
                audit.systemCancelSellTrigger(serverID, transId, session.userInfo.username, stock);

                if (stock == null || stock.length() == 0) {
                    throw new InvalidCommandException("Missing stock symbol.");
                }
                stock = stock.toUpperCase();
                debugLog.debug("TS.cancelSellTrigger: stock: " + stock);

                // Cancel any pending trigger
                session.removePendingSellTrigger(stock);

                Trigger trigger = triggerEngine.findSellTrigger(session.userInfo.username, stock);
                if (trigger != null) {
                    // Cancel real trigger
                    triggerEngine.removeSellTrigger(trigger);
                    this.factory.getDBServer().cancelSellTrigger(session.userId, trigger.id);
                    StockRecord record = (StockRecord)session.userInfo.stocks.get(stock);
                    if (record == null) {
                        System.err.println("TransactionServer.cancelSellTrigger error: missing stock record");
                    } else {
                        record.triggerReserveAmount -= (int)trigger.amount;
                        if (record.triggerReserveAmount < 0) {
                            System.err.println("TransactionServer.cancelSellTrigger error: negative triggerReserveAmount");
                            record.triggerReserveAmount = 0;
                        }

                        record.amount += (int)trigger.amount;
                    }
                }

                session.userInfo.sellTriggers.remove(stock);
                addTransactionStats(startTime);
            }
        } catch (RuntimeException err) {
            audit.errorEvent(serverID, transId, session.userInfo.username, CMDNAME, "RuntimeException: " + err.getMessage());
            debugLog.error(CMDNAME, err);
            myStats.addError();
            throw err;
        } catch (CommException err) {
            audit.errorEvent(serverID, transId, session.userInfo.username, CMDNAME, "CommException: " + err.getMessage());
            debugLog.warn(CMDNAME + err.getMessage());
            myStats.addError();
            throw err;
        } catch (InvalidCommandException err) {
            audit.errorEvent(serverID, transId, session.userInfo.username, CMDNAME, "InvalidCommandException: " + err.getMessage());
            debugLog.warn(CMDNAME + err.getMessage());
            myStats.addError();
            throw err;
        } catch (RemoteException err) {
            audit.errorEvent(serverID, transId, session.userInfo.username, CMDNAME, "RemoteException: " + err.getMessage());
            debugLog.error(CMDNAME, err);
            myStats.addError();
            throw err;
        }
    }

    public void initSystem() throws RemoteException,
                                    CommException,
                                    IOException
    {
        synchronized (sessions) {
            debugLog.debug("TS: init system");
            this.sessions.clear();
            this.sessionsByUserName.clear();
            //this.dbServer.initDatabase();
            //audit.restartLog();
            this.beginRun();
            debugLog.debug("TS: init complete");
        }
    }


    //
    // StatsCollector Interface
    //


    public void beginRun() throws RemoteException {
        debugLog.debug("Begin Stat Run");
        myStats.start();
    }

    public void endRun() throws RemoteException {
        debugLog.debug("End Stat Run");
        myStats.stop();
    }

    public Stats getStats() throws RemoteException {
        debugLog.debug("Get Stats");
        return myStats.clone();
    }


    //
    // Internal methods
    //
    private long newSessionId() {
        long id = System.currentTimeMillis() << 32;
        id += ++this.sessionIdSpool;
        return id;
    }


    protected Session getSessionById(long sessionId) throws SessionNotFoundException {
        synchronized (sessions) {
            Session s = (Session)this.sessions.get(new Long(sessionId));
            if (s == null) {
                throw new SessionNotFoundException(sessionId);
            }
            s.ping();
            return s;
        }
    }

    private void removeSession(Session s) {
        synchronized (s) {
            this.sessions.remove(s.sessionId);
            this.sessionsByUserName.remove(s.userInfo.username);
        }

        try {
            //
            // Register session with LoadBalancer/ServerRegistry
            //
            ServerManager serverManager = (ServerManager)factory.getServerRegistry();
            serverManager.unregisterSession(s.sessionId, serverID);
        } catch (RemoteException err) {
            debugLog.fatal("Failed to unregister session: RemoteException", err);
            //throw err;
        } catch (SessionNotFoundException err) {
            debugLog.fatal("Failed to unregister session: Server not registered", err);
            //throw new CommException(err.getMessage());
        } catch (CommException err) {
            debugLog.fatal("Failed to unregister session: CommException", err);
        }

    }

    private void sweepSessions() {
        synchronized (this.sessions) {
            List killList = null;
            Enumeration e = this.sessions.elements();
            while (e.hasMoreElements()) {
                Session s = (Session)e.nextElement();
                if (s.isExpired()) {
                    if (killList == null) {
                        killList = new LinkedList();
                    }
                    killList.add(s);
                }
            }

            if (killList != null) {
                Iterator i = killList.iterator();
                while (i.hasNext()) {
                    removeSession((Session)i.next());
                }
            }
        }
    }


    //
    // Inner Classes
    //
    protected class SessionSweepThread extends Thread {
        private boolean terminated = false;

        public void run() {
            int count = 0;
            while (!terminated) {
                try {
                    Thread.sleep(1000);
                    if (++count % 30 == 0) {
                        sweepSessions();
                    }
                } catch (InterruptedException err) {
                }
            }
        }

        public void terminate() {
            terminated = true;
        }
    }

    protected class Session implements Serializable {
        public TransactionServer owner;

        public Long sessionId;
        public int userId;
        public UserInfo userInfo;
        public List buyCommands = new LinkedList();
        public List sellCommands = new LinkedList();
        public long lastPing;
        public Hashtable pendingBuyTriggers = new Hashtable();
        public Hashtable pendingSellTriggers = new Hashtable();
        //public Hashtable buyTriggers = new Hashtable();
        //public Hashtable sellTriggers = new Hashtable();

        public Session(int userId, UserInfo userInfo, TransactionServer owner, long sessionId) {
            this.owner = owner;
            this.sessionId = new Long(sessionId);
            this.userInfo = userInfo;
            this.userId = userId;
            this.ping();
        }

        public void ping() {
            this.lastPing = System.currentTimeMillis();
        }

        public boolean isExpired() {
            return System.currentTimeMillis() - this.lastPing > SESSION_TIMEOUT_MS;
        }

        public void sweepBuyCommands() {
            sweepCommands(buyCommands);
        }

        public void sweepSellCommands() {
            sweepCommands(sellCommands);
        }

        private void sweepCommands(List commands) {
            debugLog.debug("Sweeping commands...");
            Iterator i = commands.iterator();
            long currentTime = System.currentTimeMillis();

            while (i.hasNext()) {
                PurchaseOrder order = (PurchaseOrder)i.next();
                if (order.expiry < currentTime) {
                    debugLog.debug("Swept " + order.quote.stock + " q: " +
                                       Long.toString(order.expiry) + ", t: " +
                                       Long.toString(currentTime));
                    i.remove();
                    cancelOrder(order);
                }
            }
        }

        public PurchaseOrder cancelBuy() throws NoPendingCommandException {

            if (this.buyCommands.isEmpty()) {
                throw new NoPendingCommandException("Buy");
            }

            PurchaseOrder order = (PurchaseOrder)this.buyCommands.remove(0);
            cancelOrder(order);
            sweepBuyCommands();
            return order;
        }

        public PurchaseOrder cancelSell() throws NoPendingCommandException {
            sweepSellCommands();

            if (this.sellCommands.isEmpty()) {
                throw new NoPendingCommandException("Sell");
            }

            PurchaseOrder order = (PurchaseOrder)this.sellCommands.remove(0);
            cancelOrder(order);
            return order;
        }

        public void addBuyCommand(PurchaseOrder order) {
            // Reserve Funds
            this.userInfo.reserveBalance = this.userInfo.reserveBalance.add(order.total);
            this.userInfo.balance = this.userInfo.balance.subtract(order.total);

            // push the buy command to the queue
            this.buyCommands.add(new PurchaseOrder(order));
        }

        public void addSellCommand(PurchaseOrder order, StockRecord existingInventory) {
            // Reserve stock amounts
            existingInventory.reserveAmount += order.amount;
            existingInventory.amount -= order.amount;
            order.existingInventory = existingInventory;

            this.sellCommands.add(new PurchaseOrder(order));
        }

        private void cancelOrder(PurchaseOrder order) {
            switch (order.kind) {
            case PurchaseOrder.BUY :
                this.userInfo.reserveBalance = this.userInfo.reserveBalance.subtract(order.total);
                this.userInfo.balance = this.userInfo.balance.add(order.total);
                break;
            case PurchaseOrder.SELL :
                debugLog.debug("Cancelling Sell...");
                order.existingInventory.reserveAmount -= order.amount;
                order.existingInventory.amount += order.amount;
            }
        }

        public void removePendingBuyTrigger(String stock) {
            Trigger oldTrigger = (Trigger)pendingBuyTriggers.get(stock);
            if (oldTrigger != null) {
                userInfo.reserveBalance = userInfo.reserveBalance.subtract(oldTrigger.amount);
                userInfo.balance = this.userInfo.balance.add(oldTrigger.amount);
                pendingBuyTriggers.remove(stock);
            }
        }

        public void addPendingBuyTrigger(Trigger trigger) {
            userInfo.reserveBalance = userInfo.reserveBalance.add(trigger.amount);
            userInfo.balance = this.userInfo.balance.subtract(trigger.amount);
            pendingBuyTriggers.put(trigger.stock, new Trigger(trigger));
        }

        public void removePendingSellTrigger(String stock) {
            TriggerWrapper oldTrigger = (TriggerWrapper)pendingSellTriggers.get(stock);
            if (oldTrigger != null) {
                oldTrigger.stockRecord.amount += oldTrigger.trigger.amount;
                oldTrigger.stockRecord.reserveAmount -= oldTrigger.trigger.amount;
                pendingSellTriggers.remove(stock);
            }
        }

        public void addPendingSellTrigger(TriggerWrapper wrapper) {
            wrapper.stockRecord.amount -= wrapper.trigger.amount;
            wrapper.stockRecord.reserveAmount += wrapper.trigger.amount;
            pendingSellTriggers.put(wrapper.trigger.stock, wrapper);
        }
    }

    private static class TriggerWrapper {
        public Trigger trigger;
        public StockRecord stockRecord;

        public TriggerWrapper(Trigger trigger, StockRecord record) {
            this.trigger = trigger;
            this.stockRecord = record;
        }

        public TriggerWrapper(TriggerWrapper src) {
            this.trigger = src.trigger;
            this.stockRecord = src.stockRecord;
        }
    }

    class TriggerEngine implements Runnable {
        private Hashtable buyTriggers = new Hashtable();
        private Hashtable buyTriggerUsers = new Hashtable();
        private Hashtable sellTriggers = new Hashtable();
        private Hashtable sellTriggerUsers = new Hashtable();

        public void addBuyTrigger(Trigger trigger) throws InvalidCommandException {

            Integer id = new Integer(trigger.id);
            if (buyTriggers.containsKey(id)) {
                throw new InvalidCommandException("Trigger id already exists");
            }

            trigger = new Trigger(trigger);
            buyTriggers.put(id, trigger);

            Hashtable userTriggers = (Hashtable)buyTriggerUsers.get(trigger.username);
            if (userTriggers == null) {
                userTriggers = new Hashtable();
                buyTriggerUsers.put(trigger.username, userTriggers);
            }
            userTriggers.put(trigger.stock, trigger);
        }

        public void addSellTrigger(Trigger trigger) throws InvalidCommandException {
            Integer id = new Integer(trigger.id);
            if (sellTriggers.containsKey(id)) {
                throw new InvalidCommandException("Trigger id already exists");
            }

            trigger = new Trigger(trigger);
            sellTriggers.put(id, trigger);

            Hashtable userTriggers = (Hashtable)sellTriggerUsers.get(trigger.username);
            if (userTriggers == null) {
                userTriggers = new Hashtable();
                sellTriggerUsers.put(trigger.username, userTriggers);
            }

            userTriggers.put(trigger.stock, trigger);
        }

        public void removeBuyTrigger(Trigger trigger) {
            buyTriggers.remove(new Integer(trigger.id));
            Hashtable userTriggers = (Hashtable)buyTriggerUsers.get(trigger.username);
            if (userTriggers != null) {
                userTriggers.remove(trigger.stock);
            }
        }

        public void removeSellTrigger(Trigger trigger) {
            sellTriggers.remove(new Integer(trigger.id));
            Hashtable userTriggers = (Hashtable)sellTriggerUsers.get(trigger.username);
            if (userTriggers != null) {
                userTriggers.remove(trigger.stock);
            }
        }

        public Trigger findBuyTrigger(String username, String stock) {
            Hashtable userTriggers = (Hashtable)buyTriggerUsers.get(username);
            if (userTriggers != null) {
                return (Trigger)userTriggers.get(stock);
            }
            return null;
        }

        public Trigger findSellTrigger(String username, String stock) {
            Hashtable userTriggers = (Hashtable)sellTriggerUsers.get(username);
            if (userTriggers != null) {
                return (Trigger)userTriggers.get(stock);
            }
            return null;
        }

        public void run() {
        }
    }

    class StatsLoggerThread extends Thread {
        private boolean terminated = false;

        public void terminate() { terminated = true; }

        public void run() {
            int count = 0;
            while (!terminated) {
                try {
                    Thread.sleep(1000);
                    if (++count % 10 == 0) {
                        if (myStats.isCollecting()) {
                            perfLog.info("NumTransactions:" + myStats.totalTransactions +
                                         " NumErrors:" + myStats.totalErrors);
                            perfLog.info("T/s (moving):" + myStats.getMovingAvgTransPerSecond() + " T/s:" +
                                         myStats.getTransactionsPerSecond() +
                                         " Ttime (moving):" + myStats.getMovingAvgTime());
                            Runtime runtime = Runtime.getRuntime();
                            perfLog.info("MaxMem:" + (runtime.maxMemory() / 1024.0f) +
                                         " TotalMem:" + (runtime.totalMemory() / 1024.0f) +
                                         " FreeMem:" + (runtime.freeMemory() / 1024.0f));
                        }
                    }
                } catch (Exception ignored) {
                }
            }
        }
    }


}
